/**
 * 
 */
package org.swing.utility.common.datetime;

import java.util.ArrayList;
import java.util.List;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.swing.utility.common.imp.DateTimeDuration;

/**
 * @author lqnhu
 *
 */
public abstract class DateTimePeriod {
	private final DateTime start;
	private final DateTime end;
	private final DateTimeDuration duration;
	// unique key representing this period such as 2009-01 or 2009-06-24 or
	// 2009-06-24-01
	private final String key;
	// long name of period such as "January 2009" or "January 1, 2009" or
	// "January 1, 2009 13:00"
	private final String longName;
	// short name of period such as "2009" or "2009-01" or "2009-06-24" or
	// "13:00" or "13:15"
	private final String shortName;
	// index name of period representing the most granular duration this period
	// supports such as "2009" for a year or "01" for a month or "24" for a day
	// or "13" or "15"
	private final String indexName;

	protected DateTimePeriod(DateTime start, DateTime end,
			DateTimeDuration duration, String keyPattern,
			String longNamePattern, String shortNamePattern,
			String indexNamePattern) {
		this.start = start;
		this.end = end;
		this.duration = duration;
		this.key = DateTimeFormat.forPattern(keyPattern).print(start);
		this.longName = DateTimeFormat.forPattern(longNamePattern).print(start);
		this.shortName = DateTimeFormat.forPattern(shortNamePattern).print(
				start);
		this.indexName = DateTimeFormat.forPattern(indexNamePattern).print(
				start);
	}

	/**
	 * Gets the inclusive starting DateTime for this period.
	 * 
	 * @return The inclusive starting DateTime for this period.
	 */
	public DateTime getStart() {
		return this.start;
	}

	/**
	 * Gets the exclusive ending DateTime for this period.
	 * 
	 * @return The exclusive ending DateTime for this period.
	 */
	public DateTime getEnd() {
		return this.end;
	}

	/**
	 * Gets the duration this period represents such as a Month, Year, etc.
	 * 
	 * @return The duration for this period
	 */
	public DateTimeDuration getDuration() {
		return this.duration;
	}

	/**
	 * Gets a key that is unique to this period. This key is unique and could be
	 * used in a HashTable, "select" box on a webpage, etc. A period can be
	 * recreated back from a key. For example:
	 * <ul>
	 * <li>The year 2009 would have a key of "2009"
	 * <li>The month January 2009 would have key of "2009-01"
	 * <li>The day January 24, 2009 would have a key of "2009-01-24"
	 * <li>The hour January 24, 2009 at 7 AM would have a key of "2009-01-24-07"
	 * <li>The hour January 24, 2009 at 2 PM would have a key of "2009-01-24-14"
	 * <li>The five minutes at January 24, 2009 at 2:15 PM would have a key of
	 * "2009-01-24-14-15"
	 * </ul>
	 * 
	 * @return A key to this period such as "2009-01" to represent a period for
	 *         January 2009.
	 */
	public String getKey() {
		return this.key;
	}

	/**
	 * Gets a long name for this period appropriate for display as the title for
	 * a report such as "January 2009". For example:
	 * <ul>
	 * <li>The year 2009 would have a long name of "2009"
	 * <li>The month January 2009 would have a long name of "January 2009"
	 * <li>The day January 24, 2009 would have a long name of "January 24, 2009"
	 * <li>The hour January 24, 2009 at 7 AM would have a long name of
	 * "January 24, 2009 07:00"
	 * <li>The hour January 24, 2009 at 2 PM would have a long name of
	 * "January 24, 2009 14:00"
	 * <li>The five minutes at January 24, 2009 at 2:15 PM would have a long
	 * name of "January 24, 2009 14:15"
	 * </ul>
	 * 
	 * @return The long name of the period such as "January 2009"
	 * @see #getShortName()
	 * @see #getIndexName()
	 */
	public String getLongName() {
		return this.longName;
	}

	/**
	 * Gets a short name for this period appropriate for display within a table
	 * of data to identify a row. A short name will be no longer than 10
	 * characters. For example:
	 * <ul>
	 * <li>The year 2009 would have a short name of "2009"
	 * <li>The month January 2009 would have a short name of "2009-01"
	 * <li>The day January 24, 2009 would have a short name of "2009-01-24"
	 * <li>The hour January 24, 2009 at 7 AM would have a short name of "07:00"
	 * <li>The hour January 24, 2009 at 2 PM would have a short name of "14:00"
	 * <li>The five minutes at January 24, 2009 at 2:15 PM would have a short
	 * name of "14:15"
	 * </ul>
	 * 
	 * @return The short name of the period such as "2009-01-24"
	 * @see #getLongName()
	 * @see #getIndexName()
	 */
	public String getShortName() {
		return this.shortName;
	}

	/**
	 * Gets an index name for this period appropriate for display to only
	 * identify the particular row by the smallest possible identifer. An index
	 * name will be no longer than 4 characters. For example:
	 * <ul>
	 * <li>The year 2009 would have an index name of "2009"
	 * <li>The month January 2009 would have an index name of "01"
	 * <li>The day January 24, 2009 would have an index name of "24"
	 * <li>The hour January 24, 2009 at 7 AM would have an index name of "07"
	 * <li>The hour January 24, 2009 at 2 PM would have an index name of "14"
	 * <li>The five minutes at January 24, 2009 at 2:15 PM would have an index
	 * name of "15"
	 * </ul>
	 * 
	 * @return The index name of the period such as "01" for January
	 * @see #getLongName()
	 * @see #getShortName()
	 */
	public String getIndexName() {
		return this.indexName;
	}

	/**
	 * Gets the next period based on the duration. For example, if this period
	 * represents a Month, this will return a period for the next Month.
	 * 
	 * @return The next period
	 */
	public abstract DateTimePeriod getNext();

	/**
	 * Gets the previous period based on the duration. For example, if this
	 * period represents a Month, this will return a period for the previous
	 * Month.
	 * 
	 * @return The previous period
	 */
	public abstract DateTimePeriod getPrevious();

	/**
	 * Gets the default "Group By" enumeration based on the duration for this
	 * period. The default "Group By" is usually the period "slice" one level
	 * below this one. For example, if this period is a month, this would return
	 * a grouping by year-month-day. If this period is a year, this would return
	 * a grouping by year-quarter.
	 * 
	 * @return A default group by determined by this period's duration
	 */
	public abstract DateTimeDuration getDefaultSubDuration();

	/**
	 * Converts this period to a list of periods for the given duration. This is
	 * an alternative way to create a list of periods instead of calling each
	 * specific duration method such as toHours() or toDays(). Partial periods
	 * will not be included.
	 * 
	 * @return A list of periods contained within this period for the specified
	 *         duration
	 * @see #toFiveMinutes()
	 * @see #toHours()
	 * @see #toDays()
	 * @see #toMonths()
	 * @see #toYears()
	 */
	public List<DateTimePeriod> toPeriods(DateTimeDuration duration) {
		if (duration == DateTimeDuration.YEAR) {
			return toYears();
		} else if (duration == DateTimeDuration.MONTH) {
			return toMonths();
		} else if (duration == DateTimeDuration.DAY) {
			return toDays();
		} else if (duration == DateTimeDuration.HOUR) {
			return toHours();
		} else if (duration == DateTimeDuration.FIVE_MINUTES) {
			return toFiveMinutes();
		} else {
			throw new UnsupportedOperationException("Duration " + duration
					+ " not supported yet");
		}
	}

	/**
	 * Converts this period to a list of five minute periods. Partial five
	 * minute periods will not be included. For example, a period of
	 * "January 20, 2009 7 to 8 AM" will return a list of 12 five minute periods
	 * - one for each 5 minute block of time 0, 5, 10, 15, etc. On the other
	 * hand, a period of "January 20, 2009 at 13:04" would return an empty list
	 * since partial five minute periods are not included.
	 * 
	 * @return A list of five minute periods contained within this period
	 */
	public List<DateTimePeriod> toFiveMinutes() {
		ArrayList<DateTimePeriod> list = new ArrayList<DateTimePeriod>();
		// default "current" five minutes to start datetime
		DateTime currentStart = getStart();
		// calculate "next" five minutes
		DateTime nextStart = currentStart.plusMinutes(5);
		// continue adding until we've reached the end
		while (nextStart.isBefore(getEnd()) || nextStart.isEqual(getEnd())) {
			// its okay to add the current
			list.add(new DateTimeFiveMinutes(currentStart, nextStart));
			// increment both
			currentStart = nextStart;
			nextStart = currentStart.plusMinutes(5);
		}
		return list;
	}

	/**
	 * Converts this period to a list of hour periods. Partial hours will not be
	 * included. For example, a period of "January 20, 2009" will return a list
	 * of 24 hours - one for each hour on January 20, 2009. On the other hand, a
	 * period of "January 20, 2009 13:10" would return an empty list since
	 * partial hours are not included.
	 * 
	 * @return A list of hour periods contained within this period
	 */
	public List<DateTimePeriod> toHours() {
		ArrayList<DateTimePeriod> list = new ArrayList<DateTimePeriod>();
		// default "current" hour to start datetime
		DateTime currentStart = getStart();
		// calculate "next" hour
		DateTime nextStart = currentStart.plusHours(1);
		// continue adding until we've reached the end
		while (nextStart.isBefore(getEnd()) || nextStart.isEqual(getEnd())) {
			// its okay to add the current
			list.add(new DateTimeHour(currentStart, nextStart));
			// increment both
			currentStart = nextStart;
			nextStart = currentStart.plusHours(1);
		}
		return list;
	}

	/**
	 * Converts this period to a list of day periods. Partial days will not be
	 * included. For example, a period of "January 2009" will return a list of
	 * 31 days - one for each day in January. On the other hand, a period of
	 * "January 20, 2009 13:00-59" would return an empty list since partial days
	 * are not included.
	 * 
	 * @return A list of day periods contained within this period
	 */
	public List<DateTimePeriod> toDays() {
		ArrayList<DateTimePeriod> list = new ArrayList<DateTimePeriod>();
		// default "current" day to start datetime
		DateTime currentStart = getStart();
		// calculate "next" day
		DateTime nextStart = currentStart.plusDays(1);
		// continue adding until we've reached the end
		while (nextStart.isBefore(getEnd()) || nextStart.isEqual(getEnd())) {
			// its okay to add the current
			list.add(new DateTimeDay(currentStart, nextStart));
			// increment both
			currentStart = nextStart;
			nextStart = currentStart.plusDays(1);
		}
		return list;
	}

	/**
	 * Converts this period to a list of month periods. Partial months will not
	 * be included. For example, a period of "2009" will return a list of 12
	 * months - one for each month in 2009. On the other hand, a period of
	 * "January 20, 2009" would return an empty list since partial months are
	 * not included.
	 * 
	 * @return A list of month periods contained within this period
	 */
	public List<DateTimePeriod> toMonths() {
		ArrayList<DateTimePeriod> list = new ArrayList<DateTimePeriod>();
		// default "current" month to start datetime
		DateTime currentStart = getStart();
		// calculate "next" month
		DateTime nextStart = currentStart.plusMonths(1);
		// continue adding until we've reached the end
		while (nextStart.isBefore(getEnd()) || nextStart.isEqual(getEnd())) {
			// its okay to add the current
			list.add(new DateTimeMonth(currentStart, nextStart));
			// increment both
			currentStart = nextStart;
			nextStart = currentStart.plusMonths(1);
		}
		return list;
	}

	/**
	 * Converts this period to a list of year periods. Partial years will not be
	 * included. For example, a period of "2009-2010" will return a list of 2
	 * years - one for 2009 and one for 2010. On the other hand, a period of
	 * "January 2009" would return an empty list since partial years are not
	 * included.
	 * 
	 * @return A list of year periods contained within this period
	 */
	public List<DateTimePeriod> toYears() {
		ArrayList<DateTimePeriod> list = new ArrayList<DateTimePeriod>();
		// default "current" year to start datetime
		DateTime currentStart = getStart();
		// calculate "next" year
		DateTime nextStart = currentStart.plusYears(1);
		// continue adding until we've reached the end
		while (nextStart.isBefore(getEnd()) || nextStart.isEqual(getEnd())) {
			// its okay to add the current
			list.add(new DateTimeYear(currentStart, nextStart));
			// increment both
			currentStart = nextStart;
			nextStart = currentStart.plusYears(1);
		}
		return list;
	}

	/**
	 * Compares another DateTimePeriod for equality if the internal start and
	 * end DateTimes equal this period.
	 * 
	 * @param object
	 *            The other DateTimePeriod
	 * @return True if the internal start and end DateTimes equal this period.
	 */
	@Override
	public boolean equals(Object object) {
		if (!(object instanceof DateTimePeriod)) {
			throw new ClassCastException(
					"Can only compare with other DateTimePeriod instances");
		}
		DateTimePeriod otherPeriod = (DateTimePeriod) object;
		if (this.start == null && otherPeriod.start != null) {
			return false;
		}
		if (this.end == null && otherPeriod.end != null) {
			return false;
		}
		if (this.start.equals(otherPeriod.start)
				&& this.end.equals(otherPeriod.end)) {
			return true;
		}
		return true;
	}

	/**
	 * Hashcode computed based on the internal start and end DateTime hashCode
	 * values.
	 * 
	 * @return A hashCode based on the hashCode of the internal start and end
	 *         DateTime values.
	 */
	@Override
	public int hashCode() {
		int hash = 5;
		hash = 59 * hash + (this.start != null ? this.start.hashCode() : 0);
		hash = 59 * hash + (this.end != null ? this.end.hashCode() : 0);
		return hash;
	}

	/**
	 * Creates a new period for the specified duration starting from the
	 * provided DateTime.
	 * 
	 * @param duration
	 *            The duration for this period such as Month, Hour, etc.
	 * @param start
	 *            The DateTime to start this period from
	 * @return A new period
	 */
	static public DateTimePeriod create(DateTimeDuration duration,
			DateTime start) {
		if (duration == DateTimeDuration.YEAR) {
			return createYear(start);
		} else if (duration == DateTimeDuration.MONTH) {
			return createMonth(start);
		} else if (duration == DateTimeDuration.DAY) {
			return createDay(start);
		} else if (duration == DateTimeDuration.HOUR) {
			return createHour(start);
		} else if (duration == DateTimeDuration.FIVE_MINUTES) {
			return createFiveMinutes(start);
		} else {
			throw new UnsupportedOperationException("Duration " + duration
					+ " not yet supported");
		}
	}

	static public DateTimePeriod createYear(int year, DateTimeZone zone) {
		// year-01-01 00:00:00.000
		DateTime start = new DateTime(year, 1, 1, 0, 0, 0, 0, zone);
		return createYear(start);
	}

	static public DateTimePeriod createYear(DateTime start) {
		DateTime end = start.plusYears(1);
		return new DateTimeYear(start, end);
	}

	static public DateTimePeriod createMonth(int year, int month,
			DateTimeZone zone) {
		// year-month-01 00:00:00.000
		DateTime start = new DateTime(year, month, 1, 0, 0, 0, 0, zone);
		return createMonth(start);
	}

	static public DateTimePeriod createMonth(DateTime start) {
		DateTime end = start.plusMonths(1);
		return new DateTimeMonth(start, end);
	}

	static public DateTimePeriod createDay(int year, int month, int day,
			DateTimeZone zone) {
		// year-month-day 00:00:00.000
		DateTime start = new DateTime(year, month, day, 0, 0, 0, 0, zone);
		return createDay(start);
	}

	static public DateTimePeriod createDay(DateTime start) {
		DateTime end = start.plusDays(1);
		return new DateTimeDay(start, end);
	}

	static public DateTimePeriod createHour(int year, int month, int day,
			int hour, DateTimeZone zone) {
		// year-month-day hour:00:00.000
		DateTime start = new DateTime(year, month, day, hour, 0, 0, 0, zone);
		return createHour(start);
	}

	static public DateTimePeriod createHour(DateTime start) {
		DateTime end = start.plusHours(1);
		return new DateTimeHour(start, end);
	}

	static public DateTimePeriod createFiveMinutes(int year, int month,
			int day, int hour, int minute, DateTimeZone zone) {
		// year-month-day hour:00:00.000
		DateTime start = new DateTime(year, month, day, hour, minute, 0, 0,
				zone);
		return createFiveMinutes(start);
	}

	static public DateTimePeriod createFiveMinutes(DateTime start) {
		DateTime end = start.plusMinutes(5);
		return new DateTimeFiveMinutes(start, end);
	}

	/**
	 * Create a list of DateTimePeriods that represent the last year of
	 * YearMonth periods. For example, if its currently January 2009, this would
	 * return periods representing
	 * "January 2009, December 2008, ... February 2008"
	 * 
	 * @param zone
	 * @return
	 */
	static public List<DateTimePeriod> createLastYearMonths(DateTimeZone zone) {
		ArrayList<DateTimePeriod> periods = new ArrayList<DateTimePeriod>();
		// get today's date
		DateTime now = new DateTime(zone);
		// start with today's current month and 11 others (last 12 months)
		for (int i = 0; i < 12; i++) {
			// create a new period
			DateTimePeriod period = createMonth(now.getYear(),
					now.getMonthOfYear(), zone);
			periods.add(period);
			// subtract 1 month
			now = now.minusMonths(1);
		}
		return periods;
	}
}
